除了前面描述的标准算术运算之外,CPU 还支持二进制之外不常见的运算。这些按位运算符直接将 5.3 逻辑门 的行为应用于比特位序列,使它们能够在硬件中直接有效地实现。与程序员通常使用加法和减法来操作变量的数值解释不同,程序员通常使用按位运算符来修改变量中的特定位。例如,程序可能会对变量中的某个位位置进行编码以保存真/假含义,并且按位运算允许程序操纵变量的各个位来更改该特定位。

4.6.1. 按位与(Bitwise AND)

按位与运算符 (&) 计算两个输入位序列。对于输入的每个数字,如果两个输入在该位置均为 1,则它会在输出的相应位置输出 1。否则,它会输出该数字 0。 表 1 显示了两个值 AB 的按位 AND 的真值表。

表 1. 按位与两个值的结果 (A AND B)

ABA & B
000
010
100
111

例如,要按位与 0b011010 与 0b110110,首先将两个序列对齐。垂直检查每个数字,如果两个数字均为 1,则将该列的结果设置为 1。否则,将该列的结果设置为 0:

        011010
    AND 110110  Only digits 1 and 4 are 1's in BOTH inputs, so
Result: 010010  those are the only digits set to 1 in the output.

要在 C 中执行按位 AND,请将 C 的按位 AND 运算符 (&) 放在两个操作数变量之间。这是用 C 语言执行的相同示例:

int x = 26;
int y = 54;

printf("Result: %d\n", x & y);  // Prints 18

按位运算与逻辑真值运算

请注意不要将按位运算符与逻辑真值运算符混为一谈。尽管名称相似(AND、OR、NOT 等),但这两个_并不_相同:

  • 按位运算符独立地考虑其输入的每一位,并根据所设置的特定输入位生成输出位序列。
  • 逻辑运算符仅考虑其操作数的 truth 解释。对于 C,零值是 false ,而所有其他值都被视为 true。评估条件(例如if语句)时经常使用逻辑运算符。

请注意,C 经常使用类似(但略有不同)的运算符来区分两者。例如,您可以分别使用单个 & 和 |来表示按位 AND 和按位 OR。逻辑 AND 和逻辑 OR 对应于双&& 和 |。最后,按位 NOT 使用~,而逻辑 NOT 使用!表示。

位 运算: 与 &, 或 |, 非 ~ 逻辑运算: 与 &&, 或 |, 非 !

4.6.2. 按位或(Bitwise OR)

按位 OR 运算符 (|) 的行为类似于按位 AND 运算符,不同之处在于,如果相应位置的输入中的一个或两个都为 1,则它会为数字输出 1。否则,它会输出该数字 0。 表 2 显示两个值 AB 按位或的真值表。

表 2. 按位或运算两个值的结果 (A OR B)

ABA | B
000
011
101
111

例如,要按位或 0b011010 与 0b110110,首先将两个序列对齐。垂直检查每个数字,如果任一数字为 1,则将该列的结果设置为 1:

        011010
     OR 110110     Only digit 0 contains a 0 in both inputs, so it's
Result: 111110     the only digit not set to 1 in the result.

要在 C 中执行按位或,请将 C 的按位或运算符 (|) 放在两个操作数之间。这是用 C 语言执行的相同示例:

int x = 26;
int y = 54;

printf("Result: %d\n", x | y);  // Prints 62

4.6.3. 按位异或(XOR, Exclusive OR)

按位 XOR 运算符 (^) 的行为类似于按位 OR 运算符,不同之处在于,仅当输入中的 恰好一个(但不是两个)在相应位置为 1 时,它才会为数字输出 1。否则,它会输出该数字 0。 表 3 显示了两个值 AB 的按位异或的真值表。

表 3. 按位异或两个值的结果 (A XOR B)

ABA ^ B
000
011
101
110

例如,要按位异或 0b011010 与 0b110110,首先将两个序列对齐。垂直检查每个数字,如果 只有一个 数字为 1,则将该列的结果设置为 1:

        011010
    XOR 110110     Digits 2, 3, and 6 contain a 1 in exactly one of
Result: 101100     the two inputs.

要在 C 中执行按位异或,请将 C 的按位异或运算符 (^) 放在两个操作数之间。这是用 C 语言执行的相同示例:

int x = 26;
int y = 54;

printf("Result: %d\n", x ^ y);  // Prints 44

4.6.4. 按位非

按位非运算符 (~) 仅对一个操作数进行运算。对于序列中的每一位,它只是翻转该位,使 0 变为 1,反之亦然。 表 4 显示按位 NOT 运算符的真值表。

表 4. 按位对值进行求值 (A) 的结果

A~ A
01
10

例如,要按位 NOT 0b011010,请反转每位的值:

    NOT 011010
Result: 100101

要在 C 中执行按位 NOT,请在操作数前面放置一个波形符 (~)。这是用 C 语言执行的相同示例:

int x = 26;

printf("Result: %d\n", ~x); // Prints -27

按位非与取反(bitwise not vs. negation)

请注意,所有现代系统都使用二进制补码来表示整数,因此按位 NOT 与求反并不完全相同。按位非(Bitwise NOT)仅翻转比特位但是不加一。

4.6.5. 移位

另一个重要的按位运算涉及将操作数位的位置左移(<<)或右移(>>)。左移和右移运算符都采用两个操作数:要移位的位序列和应移位的位数。

左移

将序列向左移动_N_位会将其每个位向左移动 N 次,从而将新的零附加到序列的右侧。例如,将八位序列 0b00101101 向左移位两位会产生 0b10110100。右侧的两个零被附加到序列的末尾,因为结果仍然需要是八位序列。

在没有溢出的情况下,向左移位 增加 结果的值,因为位向让对数字的值贡献更大的2的幂的方向移动。然而,对于固定数量的位数,任何移位到超出数字最大容量的位置的位都会被截断。例如,将八位序列 0b11110101(无符号解释 245)向左移位一位会生成 0b11101010(无符号解释 234)。这里,移出的高位被截断,使得结果变小。

要在 C 中执行左移位,请在值和要移动该值的位数之间放置两个小于字符 (<<):

int x = 13;  // 13 is 0b00001101

printf("Result: %d\n", x << 3);  // Prints 104 (0b01101000)

右移

右移类似于左移——任何移出变量容量的位(例如,从末尾向右移)都会因截断而消失。然而,右移引入了一个额外的考虑因素:结果左侧前面的新位可能需要全为零或全一,具体取决于要移位的变量的类型及其高位位值。从概念上讲,选择在前面添加零或一类似于符号扩展。因此,右移存在两种不同的变体:

  • 逻辑右移总是在结果的高位前添加零。逻辑移位用于移位 无符号 变量,因为无符号值的最高有效位中的前导 1 并不意味着该值为负数。例如,使用逻辑移位将 0b10110011 向右移动两位会产生 0b00101100。
  • 算术右移会将移位值的最高有效位的副本添加到每个新位位置中。算术移位适用于有符号变量,因此保留高位的符号非常重要。例如,使用算术移位将 0b10110011 向右移动两位会得到 0b11101100。

幸运的是,在用 C 语言编程时,如果正确声明了变量,通常不需要担心这种区别。如果您的程序包含右移位运算符 (>>),则几乎每个 C 编译器都会根据移位变量的类型自动执行适当类型的移位。也就是说,如果使用 unsigned 限定符声明移位变量,编译器将执行逻辑移位。否则,它将执行算术移位。

c 右移示例程序

您可以使用一个小示例程序来测试右移的行为,如下所示:

#include <stdio.h>

 int main(int argc, char **argv) {

</div>

 >     /* Unsigned integer value: u_val. */
 >     unsigned int u_val = 0xFF000000;
>
>      /* Signed integer value: s_val. */
>      int s_val = 0xFF000000;
>
 >   printf("%08X\n", u_val >> 12);  // logical right shift
 >   printf("%08X\n", s_val >> 12);  // arithmetic right shift
>
>   return 0;
>}
> ```
> 
> 该程序声明了两个 32 位整数:一个为无符号整数 (`u_val`),另一个为有符号整数 (`s_val`)。它将两个整数初始化为相同的起始值:由 8 个 1 组成的序列,后跟 24 个 0 (`0b1111111100000000000000000000000000`),然后将两个值向右移动 12 个位置。执行时,会打印:
> 
> ```bash
> $ ./a.out
> 000FF000
> FFFFF000
>```
>
> 由于前导 1 并不表示无符号`u_val`为“负”,因此编译器使用指令仅在其前面添加零。移位后的结果包含 12 个零、8 个 1 和另外 12 个零 (`0b00000000000011111111000000000000`)。另一方面,前导 1 **确实** 表示 `s_val` 为“负”,因此编译器会在移位值的前面添加 1,生成 20 个 1,后跟 12 个零 (`0b11111111111111111111000000000000`)。